Aboliendo el "new"
Todos los lenguajes de programación nos brindan una función canónica para la creación de instancias de una clase dada, esta palabra clave suele ser new y en varios lenguajes este preceder al nombre de la clase (PHP, C#, Java, Python o Javascript), dejemos Smalltalk a un lado por esta vez ya que en su caso es al revés.
El new en si mismo no tiene nada de malo, pero si el uso abusivo de este.
Creación estándar
Para no perder de vista el llevar a código los conceptos, vamos a tirar un poco de PHP con un Value Object que represente el Puerto de un computador/servidor:
<?php
final class Port {
private const MIN_PORT_VALUE = 0;
private const MAX_PORT_VALUE = 65535
private $port;
public function __construct(int $port) {
$this->_isValidPortOrThrow($port);
$this->port = $port;
}
public function value() :int { return $this->port;}
public function equals(self $other) :bool { return $this->port === $other->port; }
private function _isValidPortOrThrow(int $port) :void {
if($port < self::MIN_PORT_VALUE || $port > self::MAX_PORT_VALUE) {
throw new \InvalidArgumentException("Port value given is out of range [".self::MIN_PORT_VALUE."-".self::MAX_PORT_VALUE."]");
}
}
}
Entonces la creación estándar de PHP para esta clase Port sería new Port(1024)
.
Pues sucede que no siempre basta con el new
por diversas razones, una de las primeras que puede ocurrir es cuando tenemos valores específicos en el código donde se utiliza la instanciación en forma repetida haciendo referencia a un concepto que podría quedar más claro de otra forma.
Named constructors
Un caso concreto continuando con la clase puerto es el hecho de que los protocolos HTTP y HTTPS corren en el puerto 80 y 443 respectivamente, obviando este detalle especifico que hay que conocer de ante manos, el código puede ser más expresivo para cualquiera utilizando el concepto de Named constructor. A los Named constructors se les conoce también por Semantic constructor o Telescopic constructor, la utilización de uno u otro suele depender del lenguaje ya que han optado ampliamente por alguno de ellos, pero en definitiva todos refieren a lo mismo, un mecanismo sencillo (método estático) para brindar métodos de construcción específicos y expresivos, que como segunda instancia puedan llamar a otros métodos del tipo named constructor de la misma clase (de aquí lo telescópico, brindando valores por defecto para aquellos campos que no se solicitaron por parámetro), o bien, al método de construcción defacto que el lenguaje proporcione.
Si bien es cierto que es un artefacto más utilizado de cierta forma en lenguajes como PHP por no brindar una sobrecarga del constructor de la clase (ni de ningún método para el caso particular de PHP), es una estrategia que bien podría aplicarse y como comentaba nos ayudara a dar mayor expresividad y por tanto explicitando la intencionalidad de existir de cada método de construcción particular, algo que a priori con la sobrecarga no queda claro al menos en forma explicita a la hora de leer el código.
final class Port {
private const MIN_PORT_VALUE = 0;
private const MAX_PORT_VALUE = 65535
private $port;
public function __construct(int $port) {
$this->_isValidPortOrThrow($port);
$this->port = $port;
}
public static function Http() :self {
return new self(80);
}
public static function Https() :self {
return new self(443);
}
// Code continues...
}
Que a la hora de ser utilizado facilita la lectura:
<?php
// Inicialmente
$serverPort = new Port(443);
// Luego
$serverPort = Port::Https();
El referirse a esta forma como Telescopic constructor surge cuando desde un constructor semántico puede llegar a utilizarse otro constructor del mismo tipo, ampliando la cantidad de parámetros pasados inicialmente, siendo este un camino para abolir la necesidad de tener uno o varios parámetros opcionales con valores por defecto. Estos métodos logran ser mas descriptivos en cuanto a su cometido y por tanto a la razón de definir ciertos parámetros con un valor fijo.
class PersonName {
private const EMPTY_SECTION_NAME = '';
private const UNAMED_PERSON_FATHER_LASTNAME = 'Doe';
private $firstName;
private $fatherLastName;
private $motherLastName;
private function __construct(string $firstName, string $fatherLastName, string $motherLastName) {
$this->firstName = $firstName;
$this->fatherLastName = $fatherLastName;
$this->motherLastName = $motherLastName;
}
private static function _ifSectionNameEmptyThrow(?string $nameSectionValue, string $nameSection) :void {
if(empty($nameSectionValue)) {
throw new InvalidArgumentException("The given {$nameSection} is an empty string");
}
}
public static function BothLastnames(string $firstName, string $fatherLastName, string $motherLastName) :self {
self::_ifSectionNameEmptyThrow($firstName, 'firstname');
self::_ifSectionNameEmptyThrow($fatherLastName, 'father lastname');
self::_ifSectionNameEmptyThrow($motherLastName, 'mother lastname');
return new self($firstName, $fatherLastName, $motherLastName);
}
public static function SingleLastname(string $firstName, string $fatherLastName) :self {
self::_ifSectionNameEmptyThrow($firstName, 'firstname');
self::_ifSectionNameEmptyThrow($fatherLastName, 'father lastname');
return new self($firstName, self::UNAMED_PERSON_FATHER_LASTNAME, self::EMPTY_SECTION_NAME);
}
public static function AnonymousOrUnnamed(string $firstName) :self {
return self::SingleLastname($firstName, self::UNAMED_PERSON_FATHER_LASTNAME);
}
public function firstName() :string {
return "{$this->firstName}";
}
public function lastName() :string {
return "{$this->fatherLastName}".($this->motherLastName? " {$this->motherLastName}": '');
}
public function completeName() :string {
return $this->firstName()." ".$this->lastName();
}
}
Lo telescópico en este caso se observa en el uso de la función SingleLastname
dentro del otro constructor estático AnonymousOrUnnamed.
En este caso al constructor se le da una visibilidad privada, por lo tanto solo puede ser utilizado desde dentro de la clase y así controlar mejor situaciones particulares no previstas inicialmente. Un ejemplo es el hecho de al comenzar con el modelado de la clase solo haberlo hecho con firstName
y lastName
(apellido paterno) asumiendo que solo se necesitara esto, en cuanto en otro países es común tener dos apellidos(el paterno y materno) y si luego esto debe pasar a ser contemplado en nuestro código, aquí vemos un afectación interesante, ya que el exigir siempre un segundo apellido no es viable(ya que este es opcional), es por eso que el hacer privado el constructor y mover las validaciones a los constructores estáticos nos otorga mayor flexibilidad sin perder el dominio sobre construir objetos que siempre sean validos.
Aún así esto no siempre es la solución final o el fin del problema, el software evoluciona a medida que las necesidades del negocio o la realidad así lo requiere, por ende, imaginemos si ahora debemos a pasar a operar en un país donde es posible asignar mas de un nombre, si bien generalmente esto esta limitado por los registros civiles podrían ser 2, 3 o 4 nombres los permitidos, como nos impacta esto y ¿hasta que punto un uso telescópico dentro de la misma clase acrecienta en demasía el código de la misma sin aportar a su comportamiento?
Pues aquí es donde los patrones de diseño creacionales llegan a brindarnos formas bien definidas, conocidas y probadas de instanciar nuestros objetos brindándonos cada uno de ellos un beneficio sobre casos particulares que se nos presenten.
Creational Design Patterns
Estos patrones nos proporcionan varios mecanismos para la creación de objetos, dotándonos con mayor flexibilidad y fomentando/permitiendo una mayor reutilización del código.
Simple Factory
Primero que nada este no es un patrón de diseño reconocido como tal en general, si bien es una estrategia bastante utilizada y suele ser un paso intermedio para pivotar a otros patrones creacionales. Sin embargo, en cierta bibliografía se lo reconoce en algún sentido como un patrón (Head First Design Patterns by Eric Freeman and Elisabeth Robson)
Este describe una clase que tiene un método de creación que mediante uno o más bloques condicionales basándose en los parámetros del método, elige la clase de producto que instanciar y devolver.
Para este ejemplo en código procederé con C Sharp:
public abstract class User
{
public String name {get;}
public String surname {get;}
public User(String name, String surname) {
this.name = name;
this.surname = surname;
}
}
public class Customer:User
{
public Customer(String name, String surname) :base(name, surname) {}
}
public class Assistant:User
{
public Assistant(String name, String surname) :base(name, surname) {}
}
public class Administrator:User
{
public Administrator(String name, String surname) :base(name, surname) {}
}
public class UserSimpleFactory {
public User CreateUser(String type, String name, String surname) {
switch(type) {
case "customer": return new Customer(name, surname);
case "assistant": return new Assistant(name, surname);
case "admin": return new Administrator(name, surname);
default: throw new ArgumentException("User type given is not valid.");
}
}
}
// Used in this way from the different user creation clients
var user = (new UserSimpleFactory()).CreateUser("admin", "Luciano", "Thoma");
El Simple Factory puede implementarse mediante un método de instancia o de clase(estático), aunque ir a lo estático nos ahorra el tener que instanciar un objeto para poder usar el método de creación puede que nos acopla un poco a la implementación concreta y dependiendo el lenguaje nos limita la herencia sobre escritura del método estático para tener un Simple Factory de mismo producto pero que opere distinto.
Factory Method
Este patrón busca definir una interface común para la creación de objetos, aunque dejando a las subclases la decisión de como resolver la instanciación.
Es decir este busca permitirnos cumplir con el principio de diseño Open/Close que busca que las entidades en nuestro software (clases, módulos, funciones, etc) puedan estar abiertas a ser extendidas aunque cerradas a modificaciones.
Es decir que el Factory Method permite gestionar la creación de una clase o grupo de ellas en una única fabrica y que luego en caso de necesitar gestionar otra clase nueva o grupo de ellas nos ayuda a evitar la necesidad de editar el código de la actual, motivando la creación de una fabrica nueva para este segundo caso .
Supongamos que en nuestra aplicación queremos poder utilizar según ciertas situaciones un sistema/medio de logging distinto(inicialmente directo a la salida estándar o a un archivo), esto quiere decir que para poder intercambiarlo en caso de requerirlo el mismo debe cumplir con una interface, pero además debemos disponer de una forma en como crearlos a cada uno con sus particularidades, sin perder de vista que a futuro podemos querer agregar la posibilidad de un servicio de logging empresarial externo.
Vayamos al código con el párrafo anterior en mente:
// Interface general para cualquier logger
interface ILogger {
public function log(string $message) :void;
}
// Clase concreta para salida estandar
class StdoutLogger implements ILogger {
public function log(string $message) :void {
echo $message;
}
}
// Clase concreta para salvar en un archivo en el sistema
class FileLogger implements ILogger {
private $filePath;
public function __construct(string $filePath) {
$this->filePath = $filePath;
}
public function log(string $message) :void {
file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND);
}
}
//---------------------------------------------------
// Interface que toda fabrica de loggers debe cumplir
interface ILoggerFactory {
public function createLogger(): ILogger;
}
class StdoutLoggerFactory implements ILoggerFactory {
public function createLogger(): ILogger {
return new StdoutLogger();
}
}
class FileLoggerFactory implements ILoggerFactory {
private $filePath;
public function __construct(string $filePath) {
$this->filePath = $filePath;
}
public function createLogger(): ILogger {
return new FileLogger($this->filePath);
}
}
Si bien ya esta todo lo inicialmente requerido implementado, asumamos que paso un tiempo y finalmente nos solicitan implementar el loggeo vía un servicio web externo lo efectuado nos permite agregarlo sin tener que tocar nada del código preexistente.
class ProviderAbcWebServiceLogger implements ILogger {
private $endpoint;
private $userIdetifier;
private $userSecret;
public function __construct(string $endpoint, string $userIdetifier, string $userSecret) {
$this->endpoint = $endpoint;
$this->userIdetifier = $userIdetifier;
$this->userSecret = $userSecret;
}
public function log(string $message) :void {
$cURLConnection = curl_init($this->endpoint);
$postHeaderContent = ['Autorizathion' => "Bearer: {$this->userIdetifier}:{$this->userSecret}"];
curl_setopt($cURLConnection, CURLOPT_HTTPHEADER, $postHeaderContent);
$postRequestBody = ['message' => $message];
curl_setopt($cURLConnection, CURLOPT_POSTFIELDS, $postRequestBody);
curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);
$apiResponse = curl_exec($cURLConnection); // As raw text
curl_close($cURLConnection);
if(200 !== curl_getinfo($cURLConnection, CURLINFO_RESPONSE_CODE)) {
throw new LogicException("Logging action into web service was not completed. Response: {$apiResponse}");
}
}
}
class ProviderAbcWebServiceLoggerFactory implements ILoggerFactory {
private $endpoint;
private $userIdetifier;
private $userSecret;
public function __construct(string $endpoint, string $userIdetifier, string $userSecret) {
$this->endpoint = $endpoint;
$this->userIdetifier = $userIdetifier;
$this->userSecret = $userSecret;
}
public function createLogger(): ILogger {
return new ProviderAbcWebServiceLogger($this->endpoint, $this->userIdetifier, $this->userSecret);
}
}
Es oportuno aclarar también que el patrón puede ser implementado no solo con una interface (como en este ej. ILoggerFactory
) sino que dependiendo la situación puede auxiliarse en una clase abstracta para gestionar algún conjunto de pasos estándar como template (WebServiceLoggerFactory
donde el contenido de la cabecera o el cuerpo del request posiblemente cambié y por ello la necesidad a la hora de implementar un nuevo ProviderZyx) y solo dejando las particularidades a cada fabrica concreta (ProviderAbcLoggerFactory extends WebServiceLoggerProvider
del ejemplo y el nuevo ProviderZyxLoggerFactory extends WebServiceLoggerProvider
)
Builder
Este patrón aparece al rescate cuando tiene un objeto complejo que construir, sea que contenga muchas, algunas opcionales, donde algunas se vinculen entre sí o requieras construir el objeto paso a paso a medida que en tu flujo principal se obtiene lo requerido.
Para bajar a terreno el concepto usaremos C#, pensando en que durante el proceso de envíos de productos adquiridos en un paso previo en un e-commerce requerimos manejar una dirección postal.
class PostalAddress
{
public readonly String street;
public readonly UInt16? streetNumber;
public readonly Byte? floor;
public readonly String? door;
public readonly UInt16 postalCode;
public readonly String city;
public readonly String province;
public PostalAddress(
String street,
UInt16 streetNumber,
Byte? floor,
String? door,
UInt16 postalCode,
String city,
String province
)
{
this.street = street;
this.streetNumber = streetNumber;
this.floor = floor;
this.door = door;
this.postalCode = postalCode;
this.city = city;
this.province = province;
}
}
En este caso al ser una entidad con varios campos y siendo opcionales algunos de ellos según estemos en presencia de una casa o un departamento por ejemplo, nos asistiremos con una clase para construir la dirección postal.
class PostalAddressBuilder
{
private String street;
private UInt16 streetNumber;
private String stairs;
private Byte? floor;
private String? door;
private UInt16 postalCode;
private String city;
private String province;
public PostalAddressBuilder()
{
this.street = "";
this.streetNumber = null;
this.stairs = "";
this.floor = null;
this.door = "";
this.postalCode = null;
this.city = "";
this.province = "";
}
public static PostalAddressBuilder Start()
{
return new PostalAddressBuilder();
}
public PostalAddress Build()
{
bool isPostalCodeMissing = this.postalCode == null;
if (isPostalCodeMissing)
{
throw new ArgumentNullException("Postal code must be set with some of the available options into the builder");
}
return new PostalAddress(
this.street,
this.streetNumber,
this.floor,
this.door,
this.postalCode.Value,
this.city,
this.province
);
}
public PostalAddressBuilder WithEdificeAddress(
String street,
UInt16 streetNumber,
Byte floor,
String door
)
{
this.street = street;
this.streetNumber = streetNumber;
this.floor = floor;
this.door = door;
return this;
}
public PostalAddressBuilder WithHouseAddress(
String street,
UInt16 streetNumber
)
{
this.street = street;
this.streetNumber = streetNumber;
this.floor = null;
this.door = null;
return this;
}
public PostalAddressBuilder AtLocality(
UInt16 postalCode,
string city,
string province
)
{
this.postalCode = postalCode;
this.city = city;
this.province = province;
return this;
}
}
Esto nos permite a la hora de construir el PostalAddress
hacerlo de la siguiente manera:
var builder = new PostalAddressBuilder();
builder.WithHouseAddress("Wall Street", 1);
builder.AtLocality(10001, "New York City", "New York State");
var newYorkCityHousePostalAddress = builder.Build();
Si surge la necesidad desde negocio de permitir solo pasar el código postal y que el resto de la información asociada a la localidad se complete automáticamente requerimos hacer unos cambios.
Inicialmente construir una clase que brinde la capacidad de inferir a partir del código postal el nombre de la localidad y el estado en que se encuentre la misma, para el caso puntual de este ejemplo dejaremos planteada una interfaz conjunto a una implementación dummy.
class LocationPostalInformation
{
public readonly UInt16 code;
public readonly String city;
public readonly String province;
public LocationPostalInformation(UInt16 code, String city, String province)
{
this.code = code;
this.city = city;
this.province = province;
}
}
interface IPostalCodeService
{
public LocationPostalInformation PostalInformationFromCode(UInt16 postalCode);
}
class PostalCodeService :IPostalCodeService
{
public LocationPostalInformation PostalInformationFromCode(UInt16 postalCode)
{
return new LocationPostalInformation(postalCode, "Dummy city name", "Dummy province name");
};
}
Luego debemos disponibilizar un método en el builder y adaptar su constructor para que reciba al servicio como una dependencia.
class PostalCodeNotFoundException: Exception {}
class PostalAddressBuilder
{
// Other properties definition ...
private IPostalCodeService postalCodeService;
public PostalAddressBuilder(IPostalCodeService postalCodeService)
{
// Other properties assignment...
this.postalCodeService = postalCodeService;
}
public static PostalAddressBuilder Start(IPostalCodeService postalCodeService)
{
return new PostalAddressBuilder(postalCodeService);
}
// Other class methods defined before ...
public PostalAddressBuilder WithPostalCode(UInt16 postalCode)
{
try {
LocationPostalInformation postalInformation = this.postalCodeService.PostalInformationFromCode(postalCode);
this.AtLocality(
postalInformation.code,
postalInformation.city,
postalInformation.province
);
} catch (Exception exception) {
throw new PostalCodeNotFoundException($"Postalcode given {city} could not be found");
}
}
}
Luego los clientes que requieran esta funcionalidad, puede usarla de la siguiente manera
var postalCodeService = new PostalCodeService();
var builder = new PostalAddressBuilder(postalCodeService);
builder.WithEdificeAddress("Wall Street", 1, 10, "A");
builder.AtPostalCode(10001);
var newYorkCityEdificeAddress = builder.Build();
Prototype
Es un patrón de diseño que busca facilitarnos el podes duplicar/copiar/clonar un objeto determinado en su estado actual, sin la necesidad de acoplarse(conocer) a las clases especificas.
Tratare de comentar su implementación tanto en PHP como en C# con las particularidades de cada lenguaje.
Buscando darle contexto a un caso de uso que me permita bajar el concepto a tierra, siguiendo el hilo conductor del e-commerce se me ocurre que el hecho de permitirle guardar el estado actual del carrito de compras a un usuario puede ser interesante para tener luego checkpoints a los que volver en caso de arrepentirse de los productos que removió, adiciono o que modifico parte de su configuración(cantidad, color, etc). Sin embargo es cierto que en el mundo real luego pueden aplicar otras reglas de negocio que afectarían al querer regresar a estos checkpoints, como por ejemplo si el producto tiene stock o en el requerido por el usuario, si el vendedor sigue habilitado para operar y demás, esta es una simplificación funcional de todas formas.
Moviendonos al código, en PHP este patrón viene disponible por el lenguaje mediante la función global clone
y el requisito de implementar en la clase el método mágico __clone
<?php
class ShoppingCart {
//More code …
public function __clone(CartId $newId): self
{
$newCart = new self($newId);
$newCart->items = array_map(
'clone',
$this->items
);
return $newCart;
}
//More code …
}
Desde donde se requiera el ejecutar esta duplicación se lo ejecuta así:
<?php
$userShoppingCart = new ShoppingCart();
// Some lines of code ....
$shoppingCartCheckPoint = clone($userShoppingCart);
En tanto en C# también tenemos una solución provista para esto en el lenguaje mediante la interface IClonable que el objeto que lo requiera debe implementar.
// No es necesario definirla como aquí yo lo hago, es a modo de dejar clara lo que nos brinda
interface IClonablle {
public object Clone();
}
class Rectangle: ICloneable {
private int weight;
private int height;
public Square(int weight, int height) {
this.weight = weight;
this.height = height;
}
public object Clone() {
return new Rectangle(this.weight, this.height);
}
}
// From the client point of view
var square = new Rectangle(5, 10);
Rectangle squareCopy = square->Clone();
DISCLAIMER:
Cubrí aquellos artefactos o patrones de diseño que yo más utilizo en mi día a día para evolucionar falencias en el código actual para hacerlo más fácil de extender y mantener, o bien, necesidades que surgen del negocio que son más facil de afrontar mediante estas técnicas conocidas desde el momento cero.
Aclaro que deje patrones creacionales por fuera en principio por lo usual de mi utilización de ellos, como por controversias que algunos de ellos despiertan (Singleton por ejemplo).
Para terminar, este es mi enfoque y visión de como evitar el new
desperdigado en todos lados.